[JavaScript 笔记] JavaScript 异步编程
date
Apr 1, 2022
slug
[JavaScript 笔记] JavaScript 的异步模式
status
Published
tags
JavaScript
summary
type
Post
什么是同步异步
从时序来看(可参看时序图),程序运行有同步和异步两种模式,那怎么叫同步异步呢?我的总结如下:程序执行后立即返回结果(也可能是代码执行),就是同步模式;没有立即返回结果,在未来某个时刻才会返回结果,就是异步模式;各举一个例子:
同步模式:你去银行取款,叫到你的号了(开始执行了),经过一系列的手续(中间柜员在操作系统以及出款等等流程时,你是一直在柜台的),要等你取到款了离开柜台(执行结束)。
异步模式:你去麦当劳吃饭,到你点餐了(开始执行了),下好单付款就可以离开了(你不需要等到取餐的,部分结束),你可以等在旁边,你也可以刷刷手机(这里还有另一个概念:阻塞和非阻塞,本文暂不讨论),几分钟之后听到叫号了,就过去取餐(整个结束)。
总结一下关键点:执行结果是否立即返回。
同步和异步的意义
我们写的代码中,大部分都是同步代码,比如同步场景:赋值、取值、运算、条件判断等等,异步场景:文件读取、网络访问、事件等等。我想了个比喻:同步代码是你问我答,异步代码是飞鸽传书。
看起来是同步代码更快,但是,性能调优时我们经常会考虑应用这个策略:同步代码改异步代码,以提升效率。那问题来了,什么时候同步,什么时候异步?谁更快?
先分析一下优劣势:
同步代码:需要执行完成并拿到结果,才进入下一步,等待时间要看逻辑复杂程度、I/O快慢等因素。优势是调用完成就可以拿到结果,劣势是如果代码比较耗时,会阻塞代码往下执行;
异步代码:完成调用执行,即可以进入下一步,结果未来某个时刻取到。优劣势和同步正好相反,优势是代码比较耗时时,不会阻塞代码的继续执行,劣势是不能在调用完成时同步拿到结果,并且需要考虑如何获取和处理未来的结果。
可以用排除法,先看看哪些需要异步,剩下的就是同步。
- 海量数据运算、慢 I/O,并且不希望长时间阻塞代码,比如仿真模型的执行、大文件的哈希。
- 分布式应用,希望提高硬件利用率,把阻塞代码变成异步代码,例如电商系统中的消息队列(MQ).
- 不确定时间的结果,比如网络请求、用户事件,典型例子就是
Ajax
请求。
- 定时任务,比如
setTimeout
以上几种就是比较常见的异步场景,其他的场景基本同步就可以解决。而谁更快的问题,并不取决于模式,而是要看具体代码的,看计算量大小、看 I/O 快慢等等。
JavaScript 中的同步异步
JavaScript
在早期时候只是当作浏览器的脚本,随着 Ajax
技术和富应用的流行,JavaScript
成为了 Web 开发
的核心组成,异步模式也应用越来越多,语言层面的支持也越来越丰富了。还是排除法,我们这里只看
JavaScript
中的几种异步模式,每一种异步模式都有自己的特点,强力推荐阅读《你不知道的JavaScript(中卷)》第二部分内容。回调函数
在
Promise
之前,异步只能用回调函数实现。回调是最早使用,也是用得最多的异步模式,有些同学可能都没意识到它是异步的,问什么是异步。看示例:function getText(url, success) {
const xhr = new XMLHttpRequest();
xhr.onreadystatechange = function () {
if (this.readyState !== 4) return;
if (this.status == 200) {
success(this.responseText);
} else {
throw new Error(this.statusText);
}
};
xhr.open('GET', url, true);
xhr.send();
}
getText('/demo/hello.txt', function success(responseText) {
console.log(responseText);
});
这个就是最简单的基于回调函数的异步代码,一个基础的
ajax
实现,getText
调用完成时没有返回数据,数据是在回调函数 success
在未来获取。回调函数在
web 1.0
时期是够用的,到了 web 2.0
富应用出现后,有很多的不方便之处:回调地狱、代码顺序问题、信任问题等等。Promise
ES6
提供了 Promise
和 微任务
支持,让异步模式使用更加方便和高效。其实
Promise
也是一种回调,大家可以看 ES5
模拟实现,但是有个重要的区别:ES6
之后的 Promise
resolve 后,会加入微任务队列,细节可以去查阅微任务
相关主题。来个
Promise
的对比示例(可以对比上面的回调函数看看):function getTextPromise(url) {
return new Promise(function (resolve, reject) {
const xhr = new XMLHttpRequest();
xhr.onreadystatechange = function () {
if (this.readyState !== 4) return;
if (this.status == 200) {
resolve(this.responseText);
} else {
reject(new Error(this.statusText));
}
};
xhr.open('GET', url, true);
xhr.send();
});
}
getTextPromise('/demo/hello.txt')
.then((responseText) => {
console.log(responseText);
});
乍一看,感觉和回调函数差不多。简单场景下确实就是差不多的,只不过回调函数不在参数里面,而是在链式方法
then
的参数里面。但是当场景复杂之后,就能体现出 Promise
的优势。// 多个请求顺序执行
getTextPromise('/demo/hello.txt')
.then((responseText) => {
console.log(responseText);
return getTextPromise('/demo/my.txt');
})
.then((responseText) => {
console.log(responseText);
return getTextPromise('/demo/world.txt');
})
.then((responseText) => {
console.log(responseText);
});
// 多个请求并发
Promise.all([getTextPromise('/demo/hello.txt'),
getTextPromise('/demo/my.txt'), getTextPromise('/demo/world.txt')])
.then(([responseText1, responseText2, responseText3]) => {
console.log(responseText1, responseText2, responseText3);
});
// 多个请求竞争
Promise.race([getTextPromise('/demo/hello.txt'),
getTextPromise('/demo/my.txt'), getTextPromise('/demo/world.txt')])
.then((responseText) => {
console.log(responseText);
});
复杂场景用
Promise
会优雅和易读很多,不会有回调地狱,且代码顺序符合正常思维习惯,还有更多其他优点:不会多次调用,良好的错误处理机制等等。生成器(Generator)
ES6
另外还新增了一个异步工具:生成器
,见下面示例(摘录自 MDN):function* generator(i) {
yield i;
yield i + 10;
}
const gen = generator(10);
console.log(gen.next().value);
// expected output: 10
console.log(gen.next().value);
// expected output: 20
生成器函数执行过程中可以暂停和恢复,暂停时可以拿到
yield
值,恢复时还可以传值。生成器的特点是:
- 可以让生成器函数的语句暂停,同时保持当前状态。
- 可以多次暂停,这个和
Promise
不太一样,Promise
只有未完成和已完成(分成功和失败)状态。
- 生成器函数会返回一个迭代器,通过迭代器控制代码恢复执行和传值。
生成器的优势是:保持异步代码的顺序,看起来和阻塞的同步代码一样。举一个最常见的应用场景:ID 生成器。还有很多高级用法,还是推荐阅读《你不知道的JavaScript(中卷)》。
async/await
ES7
又增加了一个异步语法:async/await
,其实是个语法糖,让我们更方便地使用 Promise
,好处类似于生成器:让异步代码看起来和阻塞的同步代码一样,有正常的逻辑顺序。function getTextPromise(url) {
return new Promise(function (resolve, reject) {
const xhr = new XMLHttpRequest();
xhr.onreadystatechange = function () {
if (this.readyState !== 4) return;
if (this.status == 200) {
resolve(this.responseText);
} else {
reject(new Error(this.statusText));
}
};
xhr.open('GET', url, true);
xhr.send();
});
}
async function asyncCall() {
console.log('calling');
const result1 = await getTextPromise('/demo/hello.txt');
console.log(result1);
const result2 = await getTextPromise('/demo/world.txt');
console.log(result2);
console.log('calling end');
}
asyncCall();
async/await
之后,Promise
对象就无需使用 then
方法链来获取异步结果了,异步写法变成了同步写法,符合我们正常的逻辑思考顺序。小结
异步模式的核心就是把现在和将来连接起来,处理好它们的关系。学会了上面几种异步模式,你就对
Javascript
异步编程有了基本的认识了,最后:多多实践和总结。